package com.mongo.service;

import com.mongo.annotation.EncryptField;
import com.mongo.enums.EncryptType;
import com.mongo.func.SFunction;
import com.mongo.secret.Secret;
import com.mongo.service.impl.LambdaMongoUpdate;
import com.mongo.service.impl.LambdaQuery;
import com.mongo.util.ReflectionUtil;
import com.mongo.util.SecretFactory;
import com.mongo.util.SpringUtils;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.GridFSFindIterable;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.apache.commons.lang3.StringUtils;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.stream.Stream;

/**
 * mongo功能扩展实现
 * @author xiazhun
 * @create 2022/5/24
 */
@SuppressWarnings("all")
public abstract class BaseService<T> implements BaseMapper<T>, InitializingBean {
    private T entity;
    protected Class<T> clazz;

    /**
     * 是否需要加解密
     */
    protected static boolean encrypt = false;

    @Autowired
    protected MongoTemplate mongoTemplate;

    @Autowired
    protected GridFsTemplate gridFsTemplate;

    private MongoMapper<T> baseMapper;

    @SuppressWarnings("all")
   protected void setBaseMapper(MongoMapper<T> baseMapper) {
        this.baseMapper = baseMapper;
        this.mongoTemplate = baseMapper.mongoTemplate();
        setClazz(baseMapper.getClazz());
    }

    protected void setEntity(T entity) {
        this.entity = entity;
    }

    @PostConstruct
    private void initGridFsTemplate() {
        SimpleMongoClientDatabaseFactory simpleMongoClientDatabaseFactory = new SimpleMongoClientDatabaseFactory(getDbUrl());
        gridFsTemplate = new GridFsTemplate(simpleMongoClientDatabaseFactory, new MappingMongoConverter(simpleMongoClientDatabaseFactory, new MongoMappingContext()));
    }


    @Override
    public String getDbUrl(){
        Environment environment = SpringUtils.getBean(Environment.class);
        final String host = environment.getProperty("spring.data.mongodb.host");
        final String username = environment.getProperty("spring.data.mongodb.username");
        final String password = environment.getProperty("spring.data.mongodb.password");
        final String database = environment.getProperty("spring.data.mongodb.database");
        final String port = environment.getProperty("spring.data.mongodb.port");
        String format = "mongodb://%s:%s@%s:%s/%s";
        return String.format(format, username, password,  host, port, database);
    }

    @Override
    public MongoTemplate mongoTemplate() {
        return mongoTemplate;
    }

    @Override
    public GridFsTemplate gridFsTemplate() {
        return gridFsTemplate;
    }


    public Query createQuery() {
        return new Query();
    }

    public Update createUpdate() {
        return new Update();
    }

    public Query createQuery(Criteria criteria) {
        return new Query(criteria);
    }

    @SuppressWarnings("unchecked")
    public Class<T> getClazz() {
        if (null != this.entity) {
            this.clazz = (Class<T>) this.entity.getClass();
        } else if (null != clazz) {
            return clazz;
        } else {
            ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
            // 获取第一个类型参数的真实类型
            this.clazz = (Class<T>) pt.getActualTypeArguments()[0];
        }
        return clazz;
    }

    private void setClazz(Class<T> clazz) {
        this.clazz = clazz;
    }

    public <R> R queryOne(Query query, Class<R> tClazz, String collection) {
        return mongoTemplate.findOne(query, tClazz, collection);
    }

    public <R> List<R> list(Query query, Class<R> tClazz, String collection) {
        return mongoTemplate.find(query, tClazz, collection);
    }

    @Override
    public T queryById(String id) {
        T t = (T) mongoTemplate.findById(id, getClazz());
        return SecretFactory.decode(encrypt, t);
    }

    @Override
    public List<T> queryByIds(Collection<String> ids) {
        List<T> list = new ArrayList<>();
        ids.forEach(id -> {
            T t = queryById(id);
            list.add(t);
        });
        return list;
    }

    @Override
    public GridFSFile queryById(ObjectId id) {
        return gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id)));
    }

    @Override
    public T queryOne(SFunction<T, ?> function, Object value) {
        Field field = ReflectionUtil.findField(function);
        Assert.notNull(field, "无法获取引用字段");
        return queryOne(field.getName(), value);
    }

    @Override
    public T queryOne(Query query) {
        T t = mongoTemplate.findOne(query, getClazz());
        return SecretFactory.decode(encrypt, t);
    }

    @Override
    public GridFSFile gridQueryOne(Query query) {
        return gridFsTemplate.findOne(query);
    }

    @Override
    public GridFSFile gridQueryOne(SFunction<GridFSFile, ?> function, Object value) {
        Field field = ReflectionUtil.findField(function);
        Assert.notNull(field, "无法获取引用字段");
        Criteria criteria = Criteria.where(field.getName()).is(value);
        return gridQueryOne(criteria);
    }

    @Override
    public List<GridFSFile> gridListFind(Query query) {
        List<GridFSFile> files = new LinkedList<>();
        GridFSFindIterable gridFSFiles = gridFsTemplate.find(query);
        gridFSFiles.forEach(files::add);
        return files;
    }

    @Override
    public void gridDel(Query query) {
        gridFsTemplate.delete(query);
    }

    @Override
    public T query(LambdaQuery<T> lambdaQuery) {
        return queryOne(lambdaQuery.getQuery());
    }


    @Override
    public List<T> list(LambdaQuery<T> lambdaQuery) {
        List<T> res = new ArrayList<>();
        List<T> list = mongoTemplate.find(lambdaQuery.getQuery(), getClazz());
        for (T t : list) {
            T value = SecretFactory.decode(encrypt, t);
            res.add(value);
        }
        return res;
    }

    @Override
    public List<T> list(Criteria criteria, String collectionName) {
        List<T> res = new ArrayList<>();
        List<T> list = mongoTemplate.find(createQuery(criteria), getClazz(), collectionName);
        for (T t : list) {
            T value = SecretFactory.decode(encrypt, t);
            res.add(value);
        }
        return res;

    }

    public long update(LambdaMongoUpdate<T> lambdaMongoUpdate) {
        long res = 0;
        Query query = lambdaMongoUpdate.getQuery();
        Update update = lambdaMongoUpdate.getUpdate();
        if (query.getQueryObject().isEmpty()) {
            throw new IllegalArgumentException("条件为空");
        }
        //如果update没有设置key,value则不更新,否则会造成数据清空
        if (!update.getUpdateObject().isEmpty()) {
            res = update(query, update);
        }
        return res;
    }

    @Override
    public List<T> list() {
        List<T> res = new ArrayList<>();
        List<T> list = mongoTemplate.findAll(getClazz());
        for (T t : list) {
            T value = SecretFactory.decode(encrypt, t);
            res.add(value);
        }
        return res;
    }

    @Override
    public List<T> list(Query query) {
        List<T> res = new ArrayList<>();
        List<T> list = mongoTemplate.find(query, getClazz());
        for (T t : list) {
            T value = SecretFactory.decode(encrypt, t);
            res.add(value);
        }
        return res;
    }


    @Override
    public T insert(T t) {
        t = SecretFactory.encrypt(encrypt, t);
        return mongoTemplate.save(t);
    }

    @Override
    public DeleteResult remove(T t) {
        return mongoTemplate.remove(t);
    }

    @Override
    public long update(Query query, Update update) {
        Assert.notEmpty(query.getQueryObject(), "条件不能为空");
        Assert.notEmpty(update.getUpdateObject(), "值不能为空");
        if (encrypt) {
            org.bson.Document updateObject = update.getUpdateObject();
            Field[] filedName = SecretFactory.getFiledName(getClazz());
            for (String key : updateObject.keySet()) {
                if ("$set".equals(key)) {
                    org.bson.Document document = (org.bson.Document) updateObject.get(key);
                    for (Field field : filedName) {
                        if (null != field) {
                            String name = field.getName();
                            if (document.containsKey(name)) {
                                EncryptField annotation = field.getAnnotation(EncryptField.class);
                                if (null != annotation) {
                                    EncryptType type = annotation.type();
                                    //获取加密工具
                                    Secret secret = SecretFactory.type(type);
                                    String sKey = null;
                                    if (null != secret) {
                                        if (Stream.of(EncryptType.SM2,EncryptType.RSA).anyMatch(n->n.equals(type))){
                                            String publicKey = annotation.publicKey();
                                            Assert.notNull(publicKey,"publicKey must not be null");
                                            sKey = publicKey;
                                        }else{
                                            String keys = annotation.key();
                                            if (StringUtils.isNotBlank(keys)){
                                                sKey = keys;
                                            }
                                        }
                                        String encrypt;
                                        if (StringUtils.isNotBlank(sKey)) {
                                            encrypt = secret.encrypt(document.get(name).toString(), sKey);
                                        } else {
                                            encrypt = secret.encrypt(document.get(name).toString());
                                        }
                                        update.set(name, encrypt);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        UpdateResult upsert = mongoTemplate.upsert(query, update, getClazz());
        return upsert.getModifiedCount();
    }

    @Override
    public long remove(Query query) {
        DeleteResult remove = mongoTemplate.remove(query, getClazz());
        return remove.getDeletedCount();
    }

    @Override
    public long removeByIds(Collection<String> ids) {
        long count = 0;
        for (String id : ids) {
            long l = removeById(id);
            count += l;
        }
        return count;
    }

    @Override
    public long save(Collection<T> collection) {
        Document annotation = AnnotationUtils.findAnnotation(getClazz(), Document.class);
        if (Objects.nonNull(annotation)) {
            String collName = Optional.of(annotation.value()).orElse(annotation.collection());
            collection.forEach(n -> save(n, collName));
            return collection.size();
        }
        return 0;
    }

    @Override
    public T save(T t, String collections) {
        t = SecretFactory.encrypt(encrypt, t);
        return mongoTemplate.save(t, collections);
    }

    @Override
    public PageImpl<T> page(Query query, PageRequest pageRequest) {
        long size = size(query); // 总数
        query.with(pageRequest);
        List<T> list = list(query);
        return new PageImpl<>(list, pageRequest, size);
    }

    @Override
    public ObjectId gSave(InputStream inputStream, String filename) {
        Assert.notNull(inputStream, "object is empty");
        Assert.notNull(filename, "filename is null");
        GridFSBucket gridFSBucket = GridFSBuckets.create(mongoTemplate.getDb());
        return gridFSBucket.uploadFromStream(filename, inputStream);
    }

    @Override
    public GridFsResource download(Query query) {
        GridFSFile gridFSFile = gridQueryOne(query);
        if (Objects.nonNull(gridFSFile)) {
            GridFSBucket gridFSBucket = GridFSBuckets.create(mongoTemplate.getDb());
            GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getFilename());
            return new GridFsResource(gridFSFile, gridFSDownloadStream);
        }
        return null;
    }


    @Override
    public InputStream getStream(Query query) {
        InputStream inputStream;
        GridFsResource resource = download(query);
        try {
            inputStream = resource.getInputStream();
        } catch (Exception e) {
            return null;
        }
        return inputStream;
    }

    @Override
    public long size() {
        return mongoTemplate.count(new Query(), getClazz());
    }

    @Override
    public long size(Query query) {
        return mongoTemplate.count(query, getClazz());
    }

    public <N> String collectionName(Class<N> tClass) {
        Document annotation = AnnotationUtils.findAnnotation(tClass, Document.class);
        Assert.notNull(annotation, "没有标注Document注解");
        return annotation.value();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Class<T> entityClazz = getClazz();
        //获取实体类字段
        Field[] declaredFields = entityClazz.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            //如果实体类存在加密注解则将encrypt置为true
            EncryptField annotation = declaredField.getAnnotation(EncryptField.class);
            if (null != annotation) {
                this.encrypt = true;
                return;
            }
        }
    }
}
